在此项目中,主要介绍了如何实现一个 JIT 编译器。JIT 的主要含义是在运行时生成机器码,然后执行。

实现一个无参数函数的调用

这里,实现的代码主要是将一串字符串打印到控制台。也就是说,代码功能等同于:

std::string hello_name = "Hello, Bob";
std::cout<<hello_name;

由于 std::cout 实际上是一个对象,因此我们这里使用 write 实现打印功能,上述代码被替换为:

std::string hello_name = "Hello, Bob";
// 1 代表了 STDOUT
write(1, hello_name.c_str(), hello_name.size());

下面的工作就是使用汇编代替对 write 的调用,首先写出下面的代码:

# chunk.s
.intel_syntax noprefix

# 调用 write 系统调用(man 2 write)
# ssize_t write(int fd, const void *buf, size_t count);
mov rax, 1 # 系统调用号
# 将函数参数放到 rdi, rsi, rdx, r10, r8, r9 寄存器中
mov rdi, 1
lea rsi, [rip + 0xa]
mov rdx, 0x11
syscall
ret
.string "Hello, Your Name\n"

然后运行下面的代码查看生成的汇编:

as chunk.s -o chunk.o
objdump -M intel -D chunk.o

得到的结果为:

 0:   48 c7 c0 01 00 00 00    mov    rax,0x1
 7:   48 c7 c7 01 00 00 00    mov    rdi,0x1
 e:   48 8d 35 0a 00 00 00    lea    rsi,[rip+0xa] (1)
15:   48 c7 c2 11 00 00 00    mov    rdx,0x11 (2)
1c:   0f 05                   syscall
1e:   c3                      ret
1指向字符串的位置
20x11 代表了字符串的长度

然后将其储存到 std::vector 中:

std::vector<uint8_t> machine_code = {
    0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1
    0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1
    0x48, 0x8d, 0x35, 0x0a, 0x00, 0x00, 0x00, // lea rsi, [rip + 0xa]
    0x48, 0xc7, 0xc2, 0x11, 0x00, 0x00, 0x00, // mov rdx, 0x11
    0x0f, 0x05, // syscall
    0xc3 // ret
};

这里请看第四行第四列中的 0x11,这代表了字符串的长度。在上面的汇编代码中,我们将字符串的位置放在了代码的尾部,因此这里也直接将字符串追加到 machine_code 的尾部即可:

for(auto ch: hello_name) {
    machine_code.emplace_back(ch);
}
uint32_t len = hello_name.length();
memcpy(&machine_code[24], &len, 4); // 传入字符串的长度

现在汇编代码已经生成了,下面只需要将汇编代码拷贝到内存中执行就行了。由于内存权限的原因,汇编代码所在的区域必须是:

  • 大小必须为 sysconf(_SC_PAGE_SIZE) 的整数倍

  • 内存区域必须是可读可执行的

因此我们直接用 mmap 分配一块内存区域,然后将汇编代码拷贝过去:

size_t estimate_memory_size(size_t machine_code_size) {
    size_t b = sysconf(_SC_PAGE_SIZE);
    return (machine_code_size + b - 1) / b * b;
}

int main(){
    // ...
    size_t required_mem_size = estimate_memory_size(machine_code.size());

    uint8_t* mem = (uint8_t*)mmap(nullptr, required_mem_size, PROT_READ | PROT_WRITE | PROT_EXEC,
                                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if(mem == MAP_FAILED) {
        std::cerr << "分配内存失败\n";
        return 1;
    }

    // 拷贝汇编代码
    memcpy(mem, &machine_code[0], machine_code.size());

}

注意函数 estimate_memory_size,其中用到了一个向上取整算法 (A+B-1)/B,先除以 B 再乘以 B 是必要的,(A+B-1)/B 最后会向下取整。

最后直接执行就可以了:

auto func = reinterpret_cast<void (*)()>(mem);
func();

munmap(mem, required_mem_size);

完整的代码为:

#include <iostream>
#include <string>
#include <vector>

extern "C" {
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
}

size_t estimate_memory_size(size_t machine_code_size) {
    size_t b = sysconf(_SC_PAGE_SIZE);
    return (machine_code_size + b - 1) / b * b;
}

int main(int argc, char* argv[]) {
    std::string hello_name = "bob";

    // clang-format off
    std::vector<uint8_t> machine_code = {
        0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1
        0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1
        0x48, 0x8d, 0x35, 0x0a, 0x00, 0x00, 0x00, // lea rsi, [rip + 0xa]
        0x48, 0xc7, 0xc2, 0x11, 0x00, 0x00, 0x00, // mov rdx, 0x11
        0x0f, 0x05, // syscall
        0xc3 // ret
    };
    // clang-format on
    uint32_t len = hello_name.length();
    memcpy(&machine_code[24], &len, 4);
    for(auto ch: hello_name) {
        machine_code.emplace_back(ch);
    }

    size_t required_mem_size = estimate_memory_size(machine_code.size());

    uint8_t* mem = (uint8_t*)mmap(nullptr, required_mem_size, PROT_READ | PROT_WRITE | PROT_EXEC,
                                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if(mem == MAP_FAILED) {
        std::cerr << "分配内存失败\n";
        return 1;
    }

    memcpy(mem, &machine_code[0], machine_code.size());

    auto func = reinterpret_cast<void (*)()>(mem);
    func();

    munmap(mem, required_mem_size);
    return 0;
}

从汇编代码中调用 CPP 函数

在上面的代码中,我们运行时生成汇编代码,然后从 cpp 中调用,这里我们使用汇编代码调用 cpp 代码。大部分内容和上面的代码相同,但是汇编代码使用的是:

.intel_syntax noprefix

# 调用函数
push rbp
mov rbp, rsp
# 传入参数地址
movabs rax, 0x0
call rax

# 返回
pop rbp
ret

汇编之后的代码为:

std::vector<uint8_t> machine_code = {
      0x55,                                     // push   rbp
      0x48, 0x89, 0xe5,                         // mov    rbp,rsp
      0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs rax,0x0
      0x00, 0x00, 0x00,
      0xff, 0xd0,                               // call   rax
      0x5d,                                     // pop    rbp
      0xc3,                                     // ret
};

这里需要注意的是,movabs rax 的汇编代码为 0x48, 0xb8,后面的 8 个字节为零的内容为函数指针地址(因为 x64 计算机上指针宽度为 8 字节)。然后我们只需要将函数地址拷贝到相应的地方就行了:

uint64_t address = reinterpret_cast<uint64_t>(&test);
memcpy(&machine_code[6], &address, sizeof(address));

最后完整的代码为:

#include <iostream>
#include <string>
#include <vector>

extern "C" {
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
}

size_t estimate_memory_size(size_t machine_code_size) {
    size_t b = sysconf(_SC_PAGE_SIZE);
    return (machine_code_size + b - 1) / b * b;
}

std::vector<int> datas { 1, 2, 3 };

void test() {
    for(int data: datas) {
        std::cout << data << ' ';
    }
    std::cout << std::endl;
}

int main(int argc, char* argv[]) {
    // clang-format off
    std::vector<uint8_t> machine_code = {
          0x55,                                     // push   rbp
          0x48, 0x89, 0xe5,                         // mov    rbp,rsp
          0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs rax,0x0
          0x00, 0x00, 0x00,
          0xff, 0xd0,                               // call   rax
          0x5d,                                     // pop    rbp
          0xc3,                                     // ret
    };
    // clang-format on
    uint64_t address = reinterpret_cast<uint64_t>(&test);
    memcpy(&machine_code[6], &address, sizeof(address));

    size_t required_mem_size = estimate_memory_size(machine_code.size());

    uint8_t* mem = (uint8_t*)mmap(nullptr, required_mem_size, PROT_READ | PROT_WRITE | PROT_EXEC,
                                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if(mem == MAP_FAILED) {
        std::cerr << "分配内存失败\n";
        return 1;
    }

    memcpy(mem, &machine_code[0], machine_code.size());

    auto func = reinterpret_cast<void (*)()>(mem);
    func();

    munmap(mem, required_mem_size);
    return 0;
}
Last moify: 2025-01-17 02:01:39
Build time:2025-07-18 09:41:42
Powered By asphinx